/*
 * FTNANDC024 driver developed by Mychaela Falconia at CarrierComm, Inc.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <linux/io.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/ioport.h>
#include <linux/slab.h>
#include <linux/mtd/mtd.h>
#include <linux/mtd/partitions.h>
#include <linux/mtd/ftnandc024.h>
#include <linux/wait.h>
#include <linux/errno.h>

#include "ftnandc024_priv.h"
#include "ftnandc024_regs.h"

static void __devinit setup_regs_for_read_id(struct ftnandc024_data *cntr)
{
	uint32_t regword;

	/*
	 * The FTNANDC024 register setup established in this routine
	 * is good for issuing Read ID and ONFI Read Parameter Page
	 * commands.
	 */
	writel(ECC_NO_PARITY | ECC_BASE | ECC_ERR_MASK(0),
		cntr->io_base + ECC_CONTROL);
	writel(BUSY_RDY_LOC(6) | CMD_STS_LOC(0),
		cntr->io_base + GENERAL_SETTING_REG);
	writel(PG_SZ_2K | ATTR_ROW_CYCLE(1) | ATTR_COL_CYCLE(1) |
		ATTR_BLOCK_SIZE(1024), cntr->io_base + MEM_ATTR_SET);
	regword = readl(cntr->io_base + MEM_ATTR_SET2);
	regword &= ~VALID_PAGE_MASK;
	regword |= VALID_PAGE(1024);
	writel(regword, cntr->io_base + MEM_ATTR_SET2);
}

static int __devinit detect_nand_chip(struct ftnandc024_data *cntr,
				      struct ftnandc024_platform_data *pdata,
				      struct ftnandc024_chip_data **chip_rtn)
{
	struct ftnandc024_chip_data *chip, *no_more_chip_types;
	int rc;

	setup_regs_for_read_id(cntr);
	rc = ftnandc024_reset_nand_chip(cntr);
	if (unlikely(rc)) {
		dev_crit(cntr->dev, "Reset failed during chip detection!\n");
		return rc;
	}

	/* perform Read ID operation */
	writel(0, cntr->io_base + CMDQUEUE3(0));
	rc = ftnandc024_execute_command(cntr, CMD_INDEX(FIXFLOW_READID) |
					      CMD_SPARE_NUM(8) | CMD_BYTE_MODE);
	if (unlikely(rc)) {
		dev_err(cntr->dev, "Read ID command failed!\n");
		return rc;
	}

	/* identify our chip */
	chip = pdata->chip_types;
	no_more_chip_types = chip + pdata->num_chip_types;
	for (; chip < no_more_chip_types; chip++) {
		if (!memcmp(cntr->io_base + SPARE_SRAM, chip->id_bytes, 8))
			break;
	}
	if (chip >= no_more_chip_types) {
		dev_err(cntr->dev, "No supported NAND flash chip found\n");
		return -ENODEV;
	}

	/* we've got our chip */
	*chip_rtn = chip;
	return 0;
}

static int __devinit setup_for_nand_chip(struct ftnandc024_data *data,
					 struct ftnandc024_chip_data *chip)
{
	unsigned page_size_code, pagesize_shift;
	uint32_t regword;

	switch (chip->page_size) {
	case 512:
		page_size_code = PG_SZ_512;
		pagesize_shift = 9;
		data->smallpage_cmds = true;
		break;
	case 2048:
		page_size_code = PG_SZ_2K;
		pagesize_shift = 11;
		break;
	case 4096:
		page_size_code = PG_SZ_4K;
		pagesize_shift = 12;
		break;
	case 8192:
		page_size_code = PG_SZ_8K;
		pagesize_shift = 13;
		break;
	case 16384:
		page_size_code = PG_SZ_16K;
		pagesize_shift = 14;
		break;
	default:
		dev_err(data->dev, "unsupported page size of %u bytes\n",
			chip->page_size);
		return -EINVAL;
	}

	data->pages_per_block = 1 << chip->block_size_log2;
	dev_info(data->dev,
		 "setting up for %u blocks of %u pages of %u bytes\n",
		 chip->total_blocks, data->pages_per_block, chip->page_size);

	/* sector configuration */
	switch (chip->sector_size) {
	case 512:
		data->sector_shift = 9;
		break;
	case 1024:
		data->sector_shift = 10;
		break;
	default:
		dev_err(data->dev, "unsupported sector size of %u bytes\n",
			chip->sector_size);
		return -EINVAL;
	}
	data->sector_size = chip->sector_size;
	data->sectors_per_page = chip->page_size >> data->sector_shift;

	/* fill struct mtd_info fields */
	data->mtd.writesize_shift = pagesize_shift;
	data->mtd.erasesize_shift = pagesize_shift + chip->block_size_log2;
	data->mtd.size = (uint64_t)chip->total_blocks <<
				data->mtd.erasesize_shift;
	data->mtd.writesize = 1 << data->mtd.writesize_shift;
	data->mtd.writesize_mask = data->mtd.writesize - 1;
	data->mtd.erasesize = 1 << data->mtd.erasesize_shift;
	data->mtd.erasesize_mask = data->mtd.erasesize - 1;
	data->mtd.oobsize = chip->oob_size;
	data->mtd.oobavail = chip->oob_size;
	data->mtd.subpage_sft = chip->subpage_shift;
	data->subpage_mask = (1 << (pagesize_shift - chip->subpage_shift)) - 1;

	data->ecclayout.oobavail = chip->oob_size;
	data->ecclayout.oobfree[0].offset = 0;
	data->ecclayout.oobfree[0].length = chip->oob_size;
	data->mtd.ecclayout = &data->ecclayout;

	/* set up FTNANDC024 registers */
	writel(chip->ecc_level - 1, data->io_base + ECC_CORRECT_BIT_REG0);
	writel(chip->oob_ecc_level - 1,
		data->io_base + ECC_SP_CORRECT_BIT_REG0);
	writel(0, data->io_base + ECC_THRES_BIT_REG0);
	writel(0, data->io_base + ECC_SP_THRES_BIT_REG0);
	writel((chip->sector_size == 1024 ? ECC_BASE : 0) | ECC_EN(0) |
		ECC_ERR_MASK(0), data->io_base + ECC_CONTROL);
	writel(BUSY_RDY_LOC(6) | CMD_STS_LOC(0) | DATA_INVERT,
		data->io_base + GENERAL_SETTING_REG);
	regword = page_size_code;
	if ((data->mtd.size >> pagesize_shift) > 65536)
		regword |= ATTR_ROW_CYCLE(3);
	else
		regword |= ATTR_ROW_CYCLE(2);
	if (chip->page_size >= 2048)
		regword |= ATTR_COL_CYCLE(2);
	else
		regword |= ATTR_COL_CYCLE(1);
	regword |= ATTR_BLOCK_SIZE(data->pages_per_block);
	writel(regword, data->io_base + MEM_ATTR_SET);
	regword = readl(data->io_base + MEM_ATTR_SET2);
	regword &= ~VALID_PAGE_MASK;
	regword |= VALID_PAGE(data->pages_per_block);
	writel(regword, data->io_base + MEM_ATTR_SET2);

	return 0;
}

static int __devinit ftnandc024_probe(struct platform_device *pdev)
{
	struct ftnandc024_platform_data *pdata = pdev->dev.platform_data;
	struct ftnandc024_data *data;
	struct resource *res;
	struct ftnandc024_chip_data *chip;
	int rc, irq;

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (!res)
		return -ENXIO;
	if (resource_size(res) < DATA_SRAM_OFFSET + SRAM_BUF_SIZE) {
		dev_err(&pdev->dev, "Memory resource size is too small\n");
		return -ENXIO;
	}

	/* Allocate memory for the device structure (and zero it) */
	data = devm_kzalloc(&pdev->dev, sizeof(struct ftnandc024_data),
			    GFP_KERNEL);
	if (!data) {
		dev_err(&pdev->dev, "failed to allocate device structure.\n");
		return -ENOMEM;
	}
	data->dev = &pdev->dev;

	if (!devm_request_mem_region(&pdev->dev, res->start, resource_size(res),
				dev_name(&pdev->dev))) {
		dev_err(&pdev->dev, "request_mem_region failed\n");
		return -EBUSY;
	}
	data->io_base = devm_ioremap(&pdev->dev, res->start, REGBLOCK_SIZE);
	if (data->io_base == NULL) {
		dev_err(&pdev->dev, "ioremap failed\n");
		return -EIO;
	}
	data->data_sram = devm_ioremap(&pdev->dev,
					res->start + DATA_SRAM_OFFSET,
					SRAM_BUF_SIZE);
	if (data->data_sram == NULL) {
		dev_err(&pdev->dev, "ioremap failed\n");
		return -EIO;
	}

	dev_info(&pdev->dev, "Initializing FTNANDC024\n");
	/* begin with a full HW reset */
	writel(1, data->io_base + GLOBAL_RESET);
	/* wait for it to clear */
	while (readl(data->io_base + GLOBAL_RESET))
		;
	/* set up timing registers */
	writel(pdata->ac_timing_0, data->io_base + FL_AC_TIMING0(0));
	writel(pdata->ac_timing_1, data->io_base + FL_AC_TIMING1(0));
	writel(pdata->ac_timing_2, data->io_base + FL_AC_TIMING2(0));

	/* set up IRQ */
	irq = platform_get_irq(pdev, 0);
	if (irq >= 0) {
		writel(0, data->io_base + ECC_INTR_EN);
		writel(0, data->io_base + INTR_ENABLE);
		writel(0xFFFFFFFF, data->io_base + INTR_STATUS);
		init_waitqueue_head(&data->wq);
		rc = devm_request_irq(&pdev->dev, irq, ftnandc024_irq_handler,
					0, "ftnandc024", data);
		if (unlikely(rc)) {
			dev_err(&pdev->dev, "devm_request_irq failed\n");
			return rc;
		}
		data->use_irq = true;
	} else
		data->use_irq = false;

	/* detect the NAND chip */
	rc = detect_nand_chip(data, pdata, &chip);
	if (unlikely(rc))
		return rc;
	dev_info(&pdev->dev, "%s NAND chip %s\n",
		 pdata->num_chip_types > 1 ? "detected" : "confirmed",
		 chip->name);
	/* set it up */
	rc = setup_for_nand_chip(data, chip);
	if (unlikely(rc))
		return rc;

	/* initialize other private data */
	mutex_init(&data->mutex);
	data->page_in_buffer = -1;

	/* register ourselves with the MTD layer */
	data->mtd.owner = THIS_MODULE;
	data->mtd.name = chip->name;
	data->mtd.type = MTD_NANDFLASH;
	data->mtd.flags = MTD_CAP_NANDFLASH;
	data->mtd.erase = ftnandc024_mtd_erase_method;
	data->mtd.read = ftnandc024_mtd_read_method;
	data->mtd.write = ftnandc024_mtd_write_method;
	if (chip->oob_size) {
		data->mtd.read_oob = ftnandc024_mtd_readoob_method;
		data->mtd.write_oob = ftnandc024_mtd_writeoob_method;
	} else {
		data->mtd.read_oob = NULL;
		data->mtd.write_oob = NULL;
	}
	data->mtd.sync = NULL;
	data->mtd.block_isbad = NULL;
	data->mtd.block_markbad = NULL;
	data->mtd.writebufsize = data->mtd.writesize;

	platform_set_drvdata(pdev, data);

	rc = mtd_device_parse_register(&data->mtd, 0, 0, chip->partitions,
					chip->partition_count);
	if (unlikely(rc)) {
		mtd_device_unregister(&data->mtd);
		platform_set_drvdata(pdev, NULL);
	}
	return rc;
}

static int __devexit ftnandc024_remove(struct platform_device *pdev)
{
	struct ftnandc024_data *data = platform_get_drvdata(pdev);

	mtd_device_unregister(&data->mtd);
	return 0;
}

static struct platform_driver ftnandc024_driver = {
	.probe		= ftnandc024_probe,
	.remove		= __devexit_p(ftnandc024_remove),
	.driver		= {
		.name	= "ftnandc024",
		.owner	= THIS_MODULE,
	},
};

module_platform_driver(ftnandc024_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Mychaela Falconia");
MODULE_DESCRIPTION("Driver for FTNANDC024 high-level NAND controller");
MODULE_ALIAS("platform:ftnandc024");
